// https://github.com/ProseMirror/prosemirror-gapcursor/blob/bbbee7d483754310f63f3b18d81f5a1da1250234/src/gapcursor.ts#L1
import { Selection, NodeSelection } from 'prosemirror-state';
import { Slice, ResolvedPos, Node } from 'prosemirror-model';
import { Mappable } from 'prosemirror-transform';

export const independentForGapCursor = '@@independentForGapCursor';

/// Gap cursor selections are represented using this class. Its
/// `$anchor` and `$head` properties both point at the cursor position.
export class GapCursor extends Selection {
  /// Create a gap cursor.
  constructor($pos: ResolvedPos) {
    super($pos, $pos);
  }

  map(doc: Node, mapping: Mappable): Selection {
    let $pos = doc.resolve(mapping.map(this.head));
    return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos);
  }

  content() {
    return Slice.empty;
  }

  eq(other: Selection): boolean {
    return other instanceof GapCursor && other.head == this.head;
  }

  toJSON(): any {
    return { type: 'ksgapcursor', pos: this.head };
  }

  /// @internal
  static fromJSON(doc: Node, json: any): GapCursor {
    if (typeof json.pos != 'number') {
      throw new RangeError('Invalid input for GapCursor.fromJSON');
    }
    return new GapCursor(doc.resolve(json.pos));
  }

  /// @internal
  getBookmark() {
    return new GapBookmark(this.anchor);
  }

  /// @internal
  static valid($pos: ResolvedPos) {
    let parent = $pos.parent;
    if (parent.isTextblock || !closedBefore($pos) || !closedAfter($pos)) {
      return false;
    }
    let override = parent.type.spec.allowGapCursor;
    if (override != null) return override;
    let deflt = parent.contentMatchAt($pos.index()).defaultType;
    return deflt && deflt.isTextblock;
  }

  /// @internal
  static findGapCursorFrom($pos: ResolvedPos, dir: number, mustMove = false) {
    search: for (;;) {
      if (!mustMove && GapCursor.valid($pos)) return $pos;
      let pos = $pos.pos,
        next = null;
      // Scan up from this position
      for (let d = $pos.depth; ; d--) {
        let parent = $pos.node(d);
        if (
          dir > 0 ? $pos.indexAfter(d) < parent.childCount : $pos.index(d) > 0
        ) {
          next = parent.child(dir > 0 ? $pos.indexAfter(d) : $pos.index(d) - 1);
          break;
        } else if (d == 0) {
          return null;
        }
        pos += dir;
        let $cur = $pos.doc.resolve(pos);
        if (GapCursor.valid($cur)) return $cur;
      }

      // And then down into the next node
      for (;;) {
        let inside: Node | null = dir > 0 ? next.firstChild : next.lastChild;
        if (!inside) {
          if (
            next.isAtom &&
            !next.isText &&
            !NodeSelection.isSelectable(next)
          ) {
            $pos = $pos.doc.resolve(pos + next.nodeSize * dir);
            mustMove = false;
            continue search;
          }
          break;
        }
        next = inside;
        pos += dir;
        let $cur = $pos.doc.resolve(pos);
        if (GapCursor.valid($cur)) return $cur;
      }

      return null;
    }
  }
}

GapCursor.prototype.visible = false;
(GapCursor as any).findFrom = GapCursor.findGapCursorFrom;

// this will often error because of hot module reloading
// we can just ignore it
try {
  Selection.jsonID('ksgapcursor', GapCursor);
} catch {}

class GapBookmark {
  constructor(readonly pos: number) {}

  map(mapping: Mappable) {
    return new GapBookmark(mapping.map(this.pos));
  }
  resolve(doc: Node) {
    let $pos = doc.resolve(this.pos);
    return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos);
  }
}

function closedBefore($pos: ResolvedPos) {
  for (let d = $pos.depth; d >= 0; d--) {
    let index = $pos.index(d),
      parent = $pos.node(d);
    // At the start of this parent, look at next one
    if (index == 0) {
      if (parent.type.spec.isolating) return true;
      continue;
    }
    // See if the node before (or its first ancestor) is closed
    for (let before = parent.child(index - 1); ; before = before.lastChild!) {
      if (
        (before.childCount == 0 && !before.inlineContent) ||
        before.isAtom ||
        before.type.spec.isolating ||
        before.type.spec[independentForGapCursor]
      ) {
        return true;
      }
      if (before.inlineContent) return false;
    }
  }
  // Hit start of document
  return true;
}

function closedAfter($pos: ResolvedPos) {
  for (let d = $pos.depth; d >= 0; d--) {
    let index = $pos.indexAfter(d),
      parent = $pos.node(d);
    if (index == parent.childCount) {
      if (parent.type.spec.isolating) return true;
      continue;
    }
    for (let after = parent.child(index); ; after = after.firstChild!) {
      if (
        (after.childCount == 0 && !after.inlineContent) ||
        after.isAtom ||
        after.type.spec.isolating ||
        after.type.spec[independentForGapCursor]
      ) {
        return true;
      }
      if (after.inlineContent) return false;
    }
  }
  return true;
}
